// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package conf includes configurations related manipulations.
package conf

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"chainmaker.org/chainmaker/smarteditor/cache"
	"chainmaker.org/chainmaker/smarteditor/command"
	"chainmaker.org/chainmaker/smarteditor/common"
	"chainmaker.org/chainmaker/smarteditor/db"
	"chainmaker.org/chainmaker/smarteditor/global"

	"chainmaker.org/chainmaker/smarteditor/event"
	"chainmaker.org/chainmaker/smarteditor/logger"
	"chainmaker.org/chainmaker/smarteditor/util"

	"github.com/88250/gulu"
)

const (
	// DockerGoConSdkPath docker go contract examples path
	DockerGoConExamplesPath = "./data/contractexamples"
	// PathSeparator holds the OS-specific path separator.
	PathSeparator = string(os.PathSeparator)
	// PathListSeparator holds the OS-specific path list separator.
	PathListSeparator = string(os.PathListSeparator)
	// WideVersion holds the current Wide's version.
	WideVersion = "1.6.0"
	// CodeMirrorVer holds the current editor version.
	CodeMirrorVer = "5.1"
)

type MySqlConf struct {
	DSN          string `json:"DSN"`
	MaxLifeTime  int    `json:"MaxLifeTime"`
	MaxIdleTime  int    `json:"MaxIdleTime"`
	MaxOpenConns int    `json:"MaxOpenConns"`
	MaxIdleConns int    `json:"MaxIdleConns"`
}

type VersionUrl struct {
	Version    string `json:"version"`
	PathHost   string `json:"pathHost"`
	ServerHost string `json:"serverHost"`
}

// Configuration.
type conf struct {
	Debug bool `json:"Debug"`

	UseDocker bool `json:"UseDocker"`
	SkipAuth  bool `json:"SkipAuth"`

	GitLabOAuthServer  string `json:"GitLabOAuthServer"`
	GitLabClientId     string `json:"GitLabClientId"`
	GitLabClientSecret string `json:"GitLabClientSecret"`

	ChainMakerClientId      string `json:"ChainMakerClientId"`
	ChainMakerClientSecret  string `json:"ChainMakerClientSecret"`
	ChainMakerOAuthServer   string `json:"ChainMakerOAuthServer"`
	ChainMakerAuthUrl       string `json:"ChainMakerAuthUrl"`
	ChainMakerBaseUrl       string `json:"ChainMakerBaseUrl"`
	ChainMakerUserDetailUrl string `json:"ChainMakerUserDetailUrl"`
	ChainMakerLogoutUrl     string `json:"ChainMakerLogoutUrl"`
	SsoOAuthServer          string `json:"SsoOAuthServer"`
	SsoToken                string `json:"SsoToken"`
	FrontUrl                string `json:"FrontUrl"`

	ListenIp     string `json:"ListenIp"`
	ListenPort   int    `json:"ListenPort"`
	ListenServer string `json:"ListenServer"`

	PublicIp         string `json:"PublicIp"`
	PublicPort       int    `json:"PublicPort"`
	PublicServer     string `json:"PublicServer"`
	CookieHost       string `json:"CookieHost"`       // cookie host
	EnablePrometheus bool   `json:"EnablePrometheus"` // 是否开启prometheus监控
	PrometheusPort   int    `json:"PrometheusPort"`   // prometheus监控端口

	LogLevel          string `json:"LogLevel"`          // logging level: trace/debug/info/warn/error
	Data              string `json:"Data"`              // data directory
	RuntimeMode       string `json:"RuntimeMode"`       // runtime mode (dev/prod)
	HTTPSessionMaxAge int    `json:"HTTPSessionMaxAge"` // HTTP session max age (in second)
	//StaticResourceVersion string        `json:"StaticResourceVersion"` // version of static resources
	Locale       string        `json:"Locale"`       // default locale
	Autocomplete bool          `json:"Autocomplete"` // default autocomplete
	SiteStatCode template.HTML `json:"SiteStatCode"` // site statistic code
	GrpcPort     string        `json:"GrpcPort"`     // grpc port

	ExcludeDirectory     string `json:"ExcludeDirectory"`     //exclude directory
	ExcludeFileExtension string `json:"ExcludeFileExtension"` //exclude file extension

	ContractRunTimeout int  `json:"ContractRunTimeout"` //contract run time out (second)
	OpenLimit          bool `json:"OpenLimit"`          //open resource limit
	ResourcePoolSize   int  `json:"ResourcePoolSize"`   //resource pool size

	DockerImage  string `json:"DockerImage"`  //docker image name
	DockerCpu    string `json:"DockerCpu"`    //docker cpu limit, such as: 0.1 => 10% 1 => 100% 2 => 200%
	DockerMemory string `json:"DockerMemory"` //docker memory limit, such as: 128M

	MaxContractCount int          `json:"MaxContractCount"` //max contract count for every user
	MaxFileCount     int          `json:"MaxFileCount"`     //max file count for every contract
	GoRoot           string       `json:"GoRoot"`
	GoPath           string       `json:"GoPath"`
	TestnetEnv       bool         `json:"TestnetEnv"`
	MySqlCFG         MySqlConf    `json:"Mysql"`
	CachedUserCount  int          `json:"CachedUserCount"`
	AuditEnable      bool         `json:"AuditEnable"`
	AuditServer      string       `json:"AuditServer"`
	AuditAuth        string       `json:"AuditAuth"`
	AuditFilePrefix  string       `json:"AuditFilePrefix"`
	VersionUrlS      []VersionUrl `json:"VersionURLS"`
}

// Logger.
var log = logger.Get()

// Wide configurations.
var Wide = &conf{}

// configurations of users.
//var Users []*User

// Indicates whether Docker is available.
var Docker bool

// Docker image to run user's program
const DockerImageGo = "alpine"

// Load loads the Wide configurations from wide.json and users' configurations from users/{userId}.json.
func Load(confPath string) {
	initWide(confPath)
	InitDB(&Wide.MySqlCFG)
	cache.InitUserCache(Wide.CachedUserCount)
	//initUsers()

	_, err := exec.LookPath(command.Docker.String())
	if nil == err {
		log.Warnf("use docker")
		Docker = true
	} else {
		log.Warnf("Not found 'docker' installed, running user's code will cause security problem")
	}
}

func InitDB(mysql *MySqlConf) {
	db.InitDBConn(mysql.DSN, mysql.MaxIdleConns,
		mysql.MaxLifeTime, mysql.MaxIdleTime, mysql.MaxOpenConns)
	//
	err := db.GlobalDB.AutoMigrate(
		new(User), new(CompileInfoModel),
		new(DeployInfoModel), new(UserDeployed),
		new(UserContractHidden), new(UserContractScanned),
	).Error
	if err != nil {
		fmt.Printf("InitDB error: %s", err.Error())
		panic(err)
	}
}

// func initUsers() {
// 	f, err := os.Open(Wide.Data + PathSeparator + "users")
// 	if nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}

// 	names, err := f.Readdirnames(-1)
// 	if nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}
// 	util.CloseAndLogError(f)

// 	for _, name := range names {
// 		if strings.HasPrefix(name, ".") { // hidden files that not be created by Wide
// 			continue
// 		}

// 		if ".json" != filepath.Ext(name) { // such as backup (*.json~) not be created by Wide
// 			continue
// 		}

// 		user := &User{}

// 		bytes, _ := ioutil.ReadFile(filepath.Join(Wide.Data, "users", name))
// 		err := json.Unmarshal(bytes, user)
// 		if err != nil {
// 			log.Errorf("Parses [%s] error: %v, skip loading this user", name, err)

// 			continue
// 		}

// 		// Compatibility upgrade (1.3.0): https://github.com/b3log/wide/issues/83
// 		if "" == user.Keymap {
// 			user.Keymap = "wide"
// 		}

// 		// Compatibility upgrade (1.5.3): https://github.com/b3log/wide/issues/308
// 		if "" == user.GoBuildArgsForLinux {
// 			user.GoBuildArgsForLinux = "-i"
// 		}
// 		if "" == user.GoBuildArgsForWindows {
// 			user.GoBuildArgsForWindows = "-i"
// 		}
// 		if "" == user.GoBuildArgsForDarwin {
// 			user.GoBuildArgsForDarwin = "-i"
// 		}

// 		Users = append(Users, user)
// 	}

// 	initWorkspaceDirs()
// 	initCustomizedConfigs()
// }

func initWide(confPath string) {
	configData, err := ioutil.ReadFile(confPath)
	if nil != err {
		log.Error("init wide read config file error", err)
		os.Exit(-1)
	}

	Wide = &conf{}
	err = json.Unmarshal(configData, Wide)
	if err != nil {
		log.Error("Parses [wide.json] error: ", err)
		os.Exit(-1)
	}
	log.Debug("load conf from file. config info: \n" + string(configData))

	Wide.Autocomplete = true
	gulu.Log.SetLevel(Wide.LogLevel)

	// User Home
	home, err := gulu.OS.Home()
	if nil != err {
		log.Error("Can't get user's home, please report this issue to developer", err)
		os.Exit(-1)
	}

	log.Debugf("${user.home} [%s]", home)
	Wide.Data = strings.Replace(Wide.Data, "${home}", home, -1)
	Wide.Data = filepath.Clean(Wide.Data)
	if err := os.MkdirAll(Wide.Data+"/playground/", 0755); nil != err {
		log.Errorf("create playground directory [%s] error", err)
		os.Exit(-1)
	}
	if err := os.MkdirAll(Wide.Data+"/users/", 0755); nil != err {
		log.Errorf("create users directory [%s] error", err)
		os.Exit(-1)
	}
	if err := os.MkdirAll(Wide.Data+"/workspaces/", 0755); nil != err {
		log.Errorf("create workspaces directory [%s] error", err)
		os.Exit(-1)
	}

	Wide.ListenServer = fmt.Sprintf("http://%s:%d", Wide.ListenIp, Wide.ListenPort)
	if Wide.PublicServer == "" {
		Wide.PublicServer = fmt.Sprintf("http://%s:%d", Wide.PublicIp, Wide.PublicPort)
	}

	timeNow := strconv.FormatInt(time.Now().UnixNano(), 10)
	log.Debugf("${time} [%s]", timeNow)
	//Wide.StaticResourceVersion = strings.Replace(Wide.StaticResourceVersion, "${time}", timeNow, 1)

	global.TestnetEnv = Wide.TestnetEnv

}

// FixedTimeCheckEnv checks Wide runtime environment periodically (7 minutes).
//
// Exits process if found fatal issues (such as not found $GOPATH),
// Notifies user by notification queue if found warning issues (such as not found gocode).
func FixedTimeCheckEnv() {
	checkEnv() // check immediately

	go func() {
		tick := time.Tick(time.Minute * 7)
		for {
			select {
			case <-tick:
				checkEnv()
			}
		}
	}()
}

func checkEnv() {
	defer gulu.Panic.Recover(nil)
	cmd := exec.Command(command.Go.String(), command.Version.String())
	buf, err := cmd.CombinedOutput()
	if nil != err {
		log.Error("Not found 'go' command, please make sure Go has been installed correctly")
		os.Exit(-1)
	}

	log.Debug(string(buf))

	if "" == os.Getenv("GOPATH") {
		log.Error("Not found $GOPATH, please configure it before running Wide")
		os.Exit(-1)
	}

	gocode, err := exec.LookPath(command.GoCode.String())
	if nil != err {
		event.EventQueue <- &event.Event{Code: event.EvtCodeGocodeNotFound}
		log.Warnf("Not found gocode [%s], please install it with this command:"+
			" go get github.com/stamblerre/gocode", gocode)
	} else {
		err := exec.Command(gocode, "-s", "-sock", "tcp", "-addr", common.GoCodeServerAddr).Start()
		if err != nil {
			log.Warnf("run go code fail. err:%s", err)
			os.Exit(-1)
		}
	}

	gotools, err := exec.LookPath(command.GoTools.String())
	if nil != err {
		event.EventQueue <- &event.Event{Code: event.EvtCodeIDEStubNotFound}
		log.Warnf("Not found gotools [%s], please install it with this command: "+
			"go get github.com/visualfc/gotools", gotools)
	}
}

// GetUserWorkspace gets workspace path with the specified user id, returns "" if not found.
func GetUserWorkspace(userID string) string {
	u := GetUser(userID)
	if u != nil {
		return u.WorkspacePath()
	}
	return ""
}

// GetGoFmt gets the path of Go format tool, returns "gofmt" if not found "goimports".
func GetGoFmt(userID string) string {
	u := GetUser(userID)
	if u == nil {
		return "gofmt"
	}
	switch u.GoFormat {
	case "gofmt":
		return "gofmt"
	case "goimports":
		return gulu.Go.GetExecutableInGOBIN("goimports")
	default:
		log.Warnf("Unsupported Go Format tool [%s]", u.GoFormat)
		return "gofmt"
	}
}

func GetUser(id string) *User {
	return GetUserByIDFromDB(id)
}

func GetUserByName(name string) *User {
	return GetUserByNameFromDB(name)
}

// // initCustomizedConfigs initializes the user customized configurations.
// func initCustomizedConfigs() {
// 	for _, user := range Users {
// 		UpdateCustomizedConf(user.ID)
// 	}
// }

// UpdateCustomizedConf creates (if not exists) or updates user customized configuration files.
//
//  1. /static/users/{userId}/style.css
// func UpdateCustomizedConf(userID string) {
// 	var u *User
// 	// maybe it is a beauty of the trade-off of the another world between design and implementation
// 	u = GetUser(userID)

// 	if nil == u {
// 		return
// 	}

// 	model := map[string]interface{}{"user": u}

// 	t, err := template.ParseFiles("static/user/style.css.tmpl")
// 	if nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}

// 	dir := filepath.Clean(Wide.Data + "/static/users/" + userID)
// 	if err := os.MkdirAll(dir, 0755); nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}

// 	fileOut, err := os.Create(dir + PathSeparator + "style.css")
// 	if nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}

// 	defer util.CloseAndLogError(fileOut)

// 	if err := t.Execute(fileOut, model); nil != err {
// 		log.Error(err)
// 		os.Exit(-1)
// 	}
// }

// // initWorkspaceDirs initializes the directories of users' workspaces.
// //
// // Creates directories if not found on path of workspace.
// func initWorkspaceDirs() {
// 	paths := make([]string, 0)

// 	for _, user := range Users {
// 		paths = append(paths, filepath.SplitList(user.WorkspacePath())...)
// 	}

// 	for _, pathName := range paths {
// 		CreateWorkspaceDir(pathName)
// 	}
// }

// CreateWorkspaceDir creates (if not exists) directories on the path.
//
//  1. root directory:{path}
//  2. src directory: {path}/src
//  3. package directory: {path}/pkg
//  4. binary directory: {path}/bin
//  5. invoke contract directory: {path}/exec
//  6. deploy contract directory: {path}/build
//  7. upload contract directory: {path}/upload
func CreateWorkspaceDir(path string) bool {
	hasExisted := false
	if util.FileExists(path) {
		hasExisted = true
	}
	createDir(path)
	createDir(path + PathSeparator + "src")
	createDir(path + PathSeparator + "pkg")
	createDir(path + PathSeparator + "bin")
	createDir(path + PathSeparator + "build")
	createDir(path + PathSeparator + "deploy")
	createDir(path + PathSeparator + "upload")
	return hasExisted
}
func GetSrcDir(workspace string) string {
	return workspace + PathSeparator + "src"
}
func CreateUploadDir(workSpace string) error {
	return createDir(workSpace + PathSeparator + "upload")
}

func GetSrcContractDir(workSpace string, contractName string) string {
	return workSpace + PathSeparator + "src" + PathSeparator + contractName
}

func GetUploadContractDir(workSpace string, contractName string) string {
	return workSpace + PathSeparator + "upload" + PathSeparator + contractName
}

// GetBuildContractDir get build contract directory
func GetBuildContractDir(workSpace string, contractName string) string {
	return workSpace + PathSeparator + "build" + PathSeparator + contractName
}

// CreateBuildContractDir create build contract directory
func CreateBuildContractDir(workspace string, contractName string) (string, error) {
	buildDirectory := GetBuildContractDir(workspace, contractName)
	err := createDir(buildDirectory)
	return buildDirectory, err
}

// GetDeployContractDir get deploy contract directory
func GetDeployContractDir(workspace string, contractName string) string {
	return workspace + PathSeparator + "deploy" + PathSeparator + contractName
}

// CreateDeployContractDir create deploy contract directory
func CreateDeployContractDir(workspace string, contractName string) (string, error) {
	buildDirectory := GetDeployContractDir(workspace, contractName)
	err := createDir(buildDirectory)
	return buildDirectory, err
}

// CreateDeployHashDir create deploy hash directory
func CreateDeployHashDir(deployDirectory string, hash string) (string, error) {
	hashDirectory := filepath.Join(deployDirectory, hash)
	err := createDir(hashDirectory)
	return hashDirectory, err
}

// CreateDeployHashDir get file name of compress source code to 7z file
func GetSource7zName() string {
	return "_source_code.7z"
}

// Src get src file name
func Src() string {
	return "src"
}

func GoModulePath() string {
	return filepath.Join("pkg", "mod")
}

// GetBuildInfoName get build info file name
func GetBuildInfoName() string {
	return "_build_info"
}

// GetDeployInfoName get deploy info  file name
func GetDeployInfoName() string {
	return "_deploy_info_list"
}

// GetDeployInfoName get exec dir
func GetExecDirName(workspace string) string {
	return filepath.Join(workspace, "exec")
}

// AppendDataToDeployHashFile write hash to  hash file
func AppendDataToDeployHashFile(directory string, md5sum string) (string, error) {
	hashFile := directory + PathSeparator + "_hash"
	openHashFile, err := os.OpenFile(hashFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return "", err
	}
	defer openHashFile.Close()
	_, err = openHashFile.WriteString(md5sum)
	if err != nil {
		return "", err
	}
	_, err = openHashFile.WriteString("\n")
	if err != nil {
		return "", err
	}
	return hashFile, err
}

// ReadHashFileData read data from hash file
func ReadHashFileData(directory string) ([]byte, error) {
	hashFile := filepath.Join(directory, "_hash")
	hashFileData, err := ioutil.ReadFile(hashFile)
	return hashFileData, err
}

// RemoveBuildContractDir remove build contract directory
func RemoveBuildContractDir(workspace string, contractName string) error {
	buildDirectory := workspace + PathSeparator + "build" + PathSeparator + contractName
	return os.RemoveAll(buildDirectory)
}

func GetUploadDir(workspace string) string {
	return workspace + PathSeparator + "upload"
}

func GetBuildDir(workspace string) string {
	return workspace + PathSeparator + "build"
}

func GetDeployDir(workspace string) string {
	return workspace + PathSeparator + "deploy"
}

// createDir creates a directory on the path if it not exists.
func createDir(path string) error {
	if !util.FileExists(path) {
		if err := os.MkdirAll(path, 0775); nil != err {
			log.Error(err)
			return err
		}
		log.Debugf("Created a dir [%s]", path)
	}
	return nil
}

// CpDockGoContractExToWS copy docker go contract examples to workspace
func CpDockGoContractExToWS(path string) error {
	err := cpDir(DockerGoConExamplesPath, path)
	if err != nil {
		return err
	}
	return nil
}

func RemoveAllSkip(rootDir string, skipRelative []string) ([]string, error) {
	fds, err := ioutil.ReadDir(rootDir)
	if err != nil {
		return nil, err
	}
	saveMp := make(map[string]interface{})
	for _, temp := range skipRelative {
		saveMp[temp] = struct{}{}
	}
	var waitedDeleted []string
	for _, fd := range fds {
		_, ok := saveMp[fd.Name()]
		if ok {
			continue
		}

		waitedDeleted = append(waitedDeleted, filepath.Join(rootDir, fd.Name()))
	}
	for _, tempDir := range waitedDeleted {
		removeErr := os.RemoveAll(tempDir)
		if removeErr != nil {
			return nil, fmt.Errorf("remove %s , failed %s", tempDir, removeErr.Error())
		}
	}
	return waitedDeleted, nil
}

// cpDir copies a whole directory recursively
func cpDir(src string, dst string) error {
	var err error
	var fds []os.FileInfo
	var srcInfo os.FileInfo

	if srcInfo, err = os.Stat(src); err != nil {
		return err
	}

	if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil {
		return err
	}

	if fds, err = ioutil.ReadDir(src); err != nil {
		return err
	}
	for _, fd := range fds {
		srcFilePath := path.Join(src, fd.Name())
		dstFilePath := path.Join(dst, fd.Name())

		if fd.IsDir() {
			if err = cpDir(srcFilePath, dstFilePath); err != nil {
				return err
			}
		} else {
			if err = cpFile(srcFilePath, dstFilePath); err != nil {
				return err
			}
		}
	}
	return nil
}

// File copies a single file from src to dst
func cpFile(src, dst string) error {
	var err error
	var srcFile *os.File
	var dstFile *os.File
	var srcInfo os.FileInfo

	if srcFile, err = os.Open(src); err != nil {
		return err
	}
	defer util.CloseAndLogError(srcFile)

	if dstFile, err = os.Create(dst); err != nil {
		return err
	}
	defer util.CloseAndLogError(dstFile)

	if _, err = io.Copy(dstFile, srcFile); err != nil {
		return err
	}
	if srcInfo, err = os.Stat(src); err != nil {
		return err
	}
	return os.Chmod(dst, srcInfo.Mode())
}

// // GetEditorThemes gets the names of editor themes.
// func GetEditorThemes() []string {
// 	ret := make([]string, 0)
// 	f, _ := os.Open("static/js/overwrite/codemirror" + "/theme")
// 	names, _ := f.Readdirnames(-1)
// 	util.CloseAndLogError(f)
// 	for _, name := range names {
// 		ret = append(ret, name[:strings.LastIndex(name, ".")])
// 	}
// 	sort.Strings(ret)
// 	log.Infof("get editor themes. theme:%s", ret)
// 	return ret
// }

// // GetThemes gets the names of themes.
// func GetThemes() []string {
// 	ret := make([]string, 0)
// 	f, _ := os.Open("static/css/themes")
// 	names, _ := f.Readdirnames(-1)
// 	defer util.CloseAndLogError(f)
// 	for _, name := range names {
// 		ret = append(ret, name[:strings.LastIndex(name, ".")])
// 	}
// 	sort.Strings(ret)
// 	return ret
// }
